/**
 * This file is part of 1genia trampoline
 * Copyright (C) 2007 1genia (contact@1genia.com)
 *
 * This library is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; version 3 of the License. 
 *
 * This library is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Library General Public License for more details. 
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; see the file COPYING.TXT.  If not,
 * write to the Free Software Foundation, Inc., 51 Franklin Street,
 * Fifth Floor, Boston, MA 02110-1301, USA. 
 **/
package com.genia.toolbox.persistence.memory.criteria;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import org.apache.commons.beanutils.PropertyUtils;
import org.springframework.util.ObjectUtils;

import com.genia.toolbox.basics.bean.Pair;
import com.genia.toolbox.persistence.basis.criteria.AbstractCriteria;
import com.genia.toolbox.persistence.basis.criteria.BasicCriteriaResult;
import com.genia.toolbox.persistence.basis.manager.AbstractPersistenceManager;
import com.genia.toolbox.persistence.criteria.CriteriaType;
import com.genia.toolbox.persistence.criteria.PropertyField;
import com.genia.toolbox.persistence.criteria.field.Field;
import com.genia.toolbox.persistence.exception.AggregateException;
import com.genia.toolbox.persistence.exception.FieldDefinitionException;
import com.genia.toolbox.persistence.exception.PersistenceException;
import com.genia.toolbox.persistence.memory.criteria.criterion.MemoryCriterion;
import com.genia.toolbox.persistence.memory.criteria.field.AbstractMemoryField;
import com.genia.toolbox.persistence.memory.criteria.order.OrderComparator;

/**
 * the memory implementation of <code>Criteria</code>.
 * 
 * @param <MAINTYPE>
 *          the main type of objects to retrieve
 */
public class MemoryCriteria<MAINTYPE>
    extends AbstractCriteria<MAINTYPE>
{

  /**
   * Constructor that allow to create projections.
   * 
   * @param modele
   *          the original <code>Criteria</code>
   * @param mainCriteriaType
   *          the <code>CriteriaType</code> of the main element of the
   *          projection.
   * @param field
   *          the <code>Field</code> representing the main projection
   */
  public MemoryCriteria(final AbstractCriteria<?> modele, final CriteriaType<MAINTYPE> mainCriteriaType, final Field field)
  {
    super(modele, mainCriteriaType, field);
  }



  /**
   * Constructor.
   * 
   * @param persistenceManager
   *          the <code>MemoryPersistenceManager</code> that is used by this
   *          implementation
   * @param mainType
   *          the class of the main type of this <code>Criteria</code>
   */
  public MemoryCriteria(final AbstractPersistenceManager persistenceManager, final Class<MAINTYPE> mainType)
  {
    super(persistenceManager, mainType);
  }



  /**
   * This method returns the results of this criteria.
   * 
   * @return the results of this criteria
   * @throws PersistenceException
   *           when a persistence error occurred
   */
  public List<BasicCriteriaResult<MAINTYPE>> list()
      throws PersistenceException
  {

    final List<BasicCriteriaResult<?>> allTuples = getAllTuples();

    final List<BasicCriteriaResult<?>> innerJoinTuples = doInnerJoins(allTuples);

    final List<BasicCriteriaResult<?>> filteredTuples = filterTuples(innerJoinTuples);

    final List<List<BasicCriteriaResult<?>>> groupByTuples = groupBy(filteredTuples);

    final List<List<BasicCriteriaResult<?>>> orderedTuples = getOrderedTuples(groupByTuples);

    final List<BasicCriteriaResult<MAINTYPE>> projectedTuples = getProjectedTuples(orderedTuples);

    final List<BasicCriteriaResult<MAINTYPE>> distinctTuples = getDistinctTyples(projectedTuples);

    return distinctTuples;
  }



  /**
   * returns the lines of results, on which inner joins has been applied.
   * 
   * @param allTuples
   *          the original lines
   * @return the lines of results, on which inner joins has been applied
   * @throws PersistenceException
   *           when a persistence error occurred
   */
  private List<BasicCriteriaResult<?>> doInnerJoins(final List<BasicCriteriaResult<?>> allTuples)
      throws PersistenceException
  {
    if (getInnerJoins().isEmpty()) {
      return allTuples;
    }
    final List<BasicCriteriaResult<?>> res = new ArrayList<BasicCriteriaResult<?>>();
    for (final BasicCriteriaResult<?> line : allTuples) {
      res.addAll(doInnerJoins(line));
    }
    return res;
  }



  /**
   * returns the line of results generated from one original line, on which
   * inner joins has been applied.
   * 
   * @param line
   *          the line of result being examined
   * @return the line of results generated from one original line, on which
   *         inner joins has been applied
   * @throws FieldDefinitionException
   *           when an error occured
   */
  private List<BasicCriteriaResult<?>> doInnerJoins(BasicCriteriaResult<?> line)
      throws FieldDefinitionException
  {
    List<BasicCriteriaResult<?>> currentRes = new ArrayList<BasicCriteriaResult<?>>();
    currentRes.add(line);
    for (Entry<MemoryCriteriaType<?>, Pair<MemoryCriteriaType<?>, PropertyField<?, ?>>> currentInnerJoin : getInnerJoins().entrySet()) {
      List<BasicCriteriaResult<?>> nextRes = new ArrayList<BasicCriteriaResult<?>>();
      for (BasicCriteriaResult<?> result : currentRes) {
        Object res;
        try {
          res = PropertyUtils.getProperty(result.get(currentInnerJoin.getValue().getFirstElement()), currentInnerJoin.getValue().getSecondElement().getPropertyName());
        }
        catch (Exception e) {
          throw new FieldDefinitionException(e);
        }
        if (res != null) {
          if (res instanceof Map) {
            res = ((Map<?, ?>) res).values();
          }
          if (res instanceof Collection) {
            Collection<?> resCollection = (Collection<?>) res;
            for (Object object : resCollection) {
              @SuppressWarnings("unchecked")
              BasicCriteriaResult<?> augmentedResult = new BasicCriteriaResult(getOriginalCriteriaType(), result);
              result.put(currentInnerJoin.getKey(), object);
              nextRes.add(augmentedResult);
            }
          }
          else {
            result.put(currentInnerJoin.getKey(), res);
            nextRes.add(result);
          }
        }
      }
      currentRes = nextRes;
    }
    return currentRes;
  }



  /**
   * return the distinct elements of a query.
   * 
   * @param tuples
   *          the tuples to distinct
   * @return the distinct elements of the query
   */
  private List<BasicCriteriaResult<MAINTYPE>> getDistinctTyples(List<BasicCriteriaResult<MAINTYPE>> tuples)
  {
    if (!isDistinct()) {
      return tuples;
    }
    ArrayList<BasicCriteriaResult<MAINTYPE>> res = null;
    if (getOrders() == null || getOrders().isEmpty()) {
      res = new ArrayList<BasicCriteriaResult<MAINTYPE>>(new HashSet<BasicCriteriaResult<MAINTYPE>>(tuples));
    }
    else {
      BasicCriteriaResult<MAINTYPE> last = null;
      res = new ArrayList<BasicCriteriaResult<MAINTYPE>>();
      for (BasicCriteriaResult<MAINTYPE> current : tuples) {
        if (!ObjectUtils.nullSafeEquals(last, current)) {
          res.add(current);
          last = current;
        }
      }
    }
    return res;
  }



  /**
   * this method group the results depending on the group by clause.
   * 
   * @param tuple
   *          The tuples to group by
   * @return the results group by.
   * @throws FieldDefinitionException
   *           when there is an error in the definition of the field
   * @throws AggregateException
   *           when there is an error with an aggregate definition
   */
  private List<List<BasicCriteriaResult<?>>> groupBy(final List<BasicCriteriaResult<?>> tuple)
      throws FieldDefinitionException, AggregateException
  {
    final List<List<BasicCriteriaResult<?>>> res = new ArrayList<List<BasicCriteriaResult<?>>>();
    if (!isAggregate()) {
      for (final BasicCriteriaResult<?> line : tuple) {
        final List<BasicCriteriaResult<?>> subList = new ArrayList<BasicCriteriaResult<?>>();
        subList.add(line);
        res.add(subList);
      }
      return res;
    }
    if (getGroupBys() == null || getGroupBys().isEmpty()) {
      res.add(tuple);
      return res;
    }

    final Map<List<Object>, List<BasicCriteriaResult<?>>> groupedResults = new HashMap<List<Object>, List<BasicCriteriaResult<?>>>();
    for (final BasicCriteriaResult<?> line : tuple) {
      final List<Object> groupBy = new ArrayList<Object>();
      for (final Field field : getGroupBys()) {
        groupBy.add(((AbstractMemoryField) field).getSimpleValue(line));
      }
      List<BasicCriteriaResult<?>> subList = groupedResults.get(groupBy);
      if (subList == null) {
        subList = new ArrayList<BasicCriteriaResult<?>>();
        groupedResults.put(groupBy, subList);
      }
      subList.add(line);
    }
    res.addAll(groupedResults.values());
    return res;
  }



  /**
   * This method sort the results of this criteria.
   * 
   * @param filteredTuples
   *          the results of this criteria
   * @return the sorted results of this criteria
   */
  private List<List<BasicCriteriaResult<?>>> getOrderedTuples(final List<List<BasicCriteriaResult<?>>> filteredTuples)
  {
    if (getOrders() != null && !getOrders().isEmpty()) {
      Collections.sort(filteredTuples, new OrderComparator(getOrders()));
    }
    return filteredTuples;
  }



  /**
   * getter for the persistenceManager.
   * 
   * @return the persistenceManager
   */
  @Override
  public AbstractPersistenceManager getPersistenceManager()
  {
    return (AbstractPersistenceManager) super.getPersistenceManager();
  }



  /**
   * returns a new <code>MemoryCriteriaType</code> associated to the type
   * given in paramters.
   * 
   * @param <TYPE>
   *          the type of the <code>MemoryCriteriaType</code>
   * @param type
   *          the class of the type of the <code>MemoryCriteriaType</code>
   * @return a new <code>MemoryCriteriaType</code> associated to the type
   *         given in paramters
   */
  @Override
  protected <TYPE> MemoryCriteriaType<TYPE> newCriteriaType(final Class<TYPE> type)
  {
    return new MemoryCriteriaType<TYPE>(type);
  }



  /**
   * This element take a tuple, a list of element and their criteriaType and
   * generate a list of tuple composed of the original tuple enhanced by each of
   * the element of the list.
   * 
   * @param tuple
   *          the original tuple
   * @param toAddCriteriaType
   *          the type of the new elements to add
   * @param toAddElements
   *          the new elements to add
   * @return the list of tuple enhanced by the new elements
   */
  @SuppressWarnings("unchecked")
  private List<BasicCriteriaResult<?>> addElementToTuple(final BasicCriteriaResult<?> tuple, final CriteriaType<?> toAddCriteriaType, final List<?> toAddElements)
  {
    final List<BasicCriteriaResult<?>> res = new LinkedList<BasicCriteriaResult<?>>();
    for (final Object persistable : toAddElements) {
      final BasicCriteriaResult<?> line = new BasicCriteriaResult(getOriginalCriteriaType(), tuple);
      line.put(toAddCriteriaType, persistable);
      res.add(line);
    }
    return res;
  }



  /**
   * return the remaining line of results, once the filters of the queries have
   * been applied.
   * 
   * @param innerJoinTuples
   *          the original lines
   * @return return the remaining line of results, once the filters of the
   *         queries have been applied
   * @throws PersistenceException
   *           when a persistence error occurred
   */
  private List<BasicCriteriaResult<?>> filterTuples(final List<BasicCriteriaResult<?>> innerJoinTuples)
      throws PersistenceException
  {
    if (getRestrictions() == null) {
      return innerJoinTuples;
    }
    final List<BasicCriteriaResult<?>> res = new ArrayList<BasicCriteriaResult<?>>();
    for (final BasicCriteriaResult<?> line : innerJoinTuples) {
      if (((MemoryCriterion) getRestrictions()).match(getPersistenceManager(), this, line)) {
        res.add(line);
      }
    }
    return res;
  }



  /**
   * Return the list of tuples that is describe by the main query of this
   * <code>Criteria</code>.
   * 
   * @return the list of tuples that is describe by the main query of this
   *         <code>Criteria</code>
   * @throws PersistenceException
   *           when the is a problem with the persisence layer
   */
  private List<BasicCriteriaResult<?>> getAllTuples()
      throws PersistenceException
  {
    return getAllTuples(getBaseQuery());
  }



  /**
   * Return the list of tuple composed of each element of the class of the list
   * given in parameter.
   * 
   * @param objects
   *          the list of type that will composed the result tuples
   * @return the list of tuple composed of each element of the class of the list
   *         given in parameter
   * @throws PersistenceException
   *           when the is a problem with the persisence layer
   */
  @SuppressWarnings("unchecked")
  private List<BasicCriteriaResult<?>> getAllTuples(final Collection<? extends CriteriaType<?>> objects)
      throws PersistenceException
  {
    if (objects.isEmpty()) {
      final BasicCriteriaResult<?> base = new BasicCriteriaResult(getOriginalCriteriaType());
      final List<BasicCriteriaResult<?>> res = new LinkedList<BasicCriteriaResult<?>>();
      res.add(base);
      return res;
    }

    final List<CriteriaType<?>> remainingObject = new LinkedList<CriteriaType<?>>(objects);
    final CriteriaType<?> toAddCriteriaType = remainingObject.get(0);
    final List<?> toAddElements = getPersistenceManager().getAll(toAddCriteriaType.getType());

    remainingObject.remove(0);

    final List<BasicCriteriaResult<?>> res = new ArrayList<BasicCriteriaResult<?>>();

    for (final BasicCriteriaResult<?> tuple : getAllTuples(remainingObject)) {
      res.addAll(addElementToTuple(tuple, toAddCriteriaType, toAddElements));
    }

    return res;
  }



  /**
   * This method projects the tuples in argument using the projections defined
   * by this criteria.
   * 
   * @param tuples
   *          the tuples to project
   * @return the projection of the tuples in argument
   * @throws FieldDefinitionException
   *           when there is an error in the definition of the field
   * @throws AggregateException
   *           when there is an error with an aggregate definition
   */
  @SuppressWarnings("unchecked")
  private List<BasicCriteriaResult<MAINTYPE>> getProjectedTuples(final List<List<BasicCriteriaResult<?>>> tuples)
      throws FieldDefinitionException, AggregateException
  {
    final List<BasicCriteriaResult<MAINTYPE>> res = new LinkedList<BasicCriteriaResult<MAINTYPE>>();
    if (getProjections() == null) {
      if (isAggregate()) {
        throw new AggregateException();
      }
      for (final List<BasicCriteriaResult<?>> lines : tuples) {
        res.add((BasicCriteriaResult<MAINTYPE>) lines.get(0));
      }
      return res;
    }
    else {
      for (final List<BasicCriteriaResult<?>> line : tuples) {
        final BasicCriteriaResult<MAINTYPE> newLine = new BasicCriteriaResult<MAINTYPE>(getMainType());
        for (final Entry<? extends CriteriaType<?>, ? extends Field> entry : getProjections().entrySet()) {
          newLine.put(entry.getKey(), ((AbstractMemoryField) entry.getValue()).getValue(line));
        }
        res.add(newLine);
      }
      return res;
    }
  }



  /**
   * Create a new <code>Criteria</code> that is the projection of this
   * <code>Criteria</code> on the <code>Field</code> passed in argument.
   * 
   * @param <TYPE>
   *          the type of the field to project to
   * @param type
   *          the class of the type of the field to project to
   * @param field
   *          the field to project to
   * @return the new <code>Criteria</code> representing the projected query.
   */
  public <TYPE> MemoryCriteria<TYPE> project(final Class<TYPE> type, final Field field)
  {
    return new MemoryCriteria<TYPE>(this, newCriteriaType(type), field);
  }



  /**
   * Create a new <code>Criteria</code> that is the projection of this
   * <code>Criteria</code> on the object defined by a
   * <code>CriteriaType</code>. This method is used only to project to a type
   * that has been created with <code>join</code>.
   * 
   * @param <TYPE>
   *          the type of the field to project to
   * @param criteriaType
   *          the <code>CriteriaType</code> of the field to add in the query.
   *          This <code>CriteriaType</code> must already be an existing
   *          <code>CriteriaType</code> of this criteria.
   * @return the new <code>Criteria</code> representing the projected query.
   */
  public <TYPE> MemoryCriteria<TYPE> project(final CriteriaType<TYPE> criteriaType)
  {
    return new MemoryCriteria<TYPE>(this, criteriaType, getPersistenceManager().getFieldManager().getObjectField(criteriaType));
  }



  /**
   * getter for the innerJoins property.
   * 
   * @return the innerJoins
   */
  @SuppressWarnings("unchecked")
  @Override
  protected Map<MemoryCriteriaType<?>, Pair<MemoryCriteriaType<?>, PropertyField<?, ?>>> getInnerJoins()
  {
    return (Map<MemoryCriteriaType<?>, Pair<MemoryCriteriaType<?>, PropertyField<?, ?>>>) super.getInnerJoins();
  }

}
